﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using PostscriptTools.Classifier;

// implements a token tagger for the postscript language

namespace PostscriptTools {
	[Export(typeof(ITaggerProvider))]
	[ContentType("postscript")]
	[TagType(typeof(PostscriptTokenTag))]
	internal sealed class PostscriptTaggerProvider : ITaggerProvider {
		public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag {
			// create a single tagger for each buffer.
			Func<ITagger<T>> sc = delegate()
			{
				return new PostscriptTokenTagger(buffer) as ITagger<T>;
			};
			return buffer.Properties.GetOrCreateSingletonProperty<ITagger<T>>(sc);
		}
	}

	public class PostscriptTokenTag : ITag {
		public PostscriptToken Type { get; private set; }

		public PostscriptTokenTag(PostscriptToken type) {
			Type = type;
		}
	}

	public class PostscriptNamedTag : PostscriptTokenTag {
		public string Name { get; private set; }

		public PostscriptNamedTag(PostscriptToken type, string name) : base(type) {
			Name = name;
		}
	}

	public class PostscriptErrorTag : PostscriptTokenTag {
		public string Message { get; private set; }

		public PostscriptErrorTag(string message): base(PostscriptToken.PsError) {
			Message = message;
		}
	}


	/// <summary>
	/// PostscriptTokenTagger is the core parser for postscript. It handles continuation lines
	/// and finds both language elements and errors within the given SnapshotSpan(s)
	/// </summary>
	internal sealed class PostscriptTokenTagger : ITagger<PostscriptTokenTag>, IDisposable {
		readonly ITextBuffer _buffer;
		ITextSnapshot _lineCacheSnapshot = null; // the snapshot that goes with _lineCache
		class StartState {
			private LineState _bolLineState;
			private int _bolStringNestingLevel;

			public StartState(LineState state, int level) {
				this._bolLineState = state;
				this._bolStringNestingLevel = level;
			}
			public void SetBol(Tuple<LineState, int> bol) {
				this._bolStringNestingLevel = bol.Item2;
				this._bolLineState = bol.Item1;
			}
			public Tuple<LineState, int> GetBol() {
				return new Tuple<LineState, int>(this._bolLineState, this._bolStringNestingLevel);
			}
			public bool Equal(Tuple<LineState, int> compareTo) {
				return ((compareTo.Item1 == _bolLineState) && (compareTo.Item2 == _bolStringNestingLevel));
			}
		}

		readonly List<StartState> _lineCache; // a list of states and the string recursive depth, one for each line. This is the startmode for scanning a line 

		private static readonly Dictionary<string, string> _definedPostscriptNames = new Dictionary<string, string>(); // key is name, value is linenumbers where they are defined

		static public Dictionary<string, string> DefinedPostscriptNames { get { return _definedPostscriptNames; } }
		private readonly LazyEventHandler<TextContentChangedEventArgs> _lazyEventHandler;

		internal PostscriptTokenTagger(ITextBuffer buffer) {
			_buffer = buffer;
			// Populate our cache initially.
			ITextSnapshot snapshot = _buffer.CurrentSnapshot;
			// we found a VS bug: if the last line does not terminate in \r\n, it does not get counted!
			_lineCache = new List<StartState>(snapshot.LineCount + 1); // just to be safe
			for (int counter = 0; counter < snapshot.LineCount + 1; counter++) {
				_lineCache.Add(new StartState(LineState.SingleLine, 0)); // initialize all our lines to be single line constructs
			}
			RescanLines(snapshot, startLine: 0, lastDirtyLine: snapshot.LineCount - 1); // initial build of _Linecache (poor last line)
			_lineCacheSnapshot = snapshot;

			// Listen for text changes so we can update our _lineCache
			int UpdateInterval = 500;
			if (snapshot.Length > 10000) {
				UpdateInterval = 1000;
			}
			this._lazyEventHandler = new LazyEventHandler<TextContentChangedEventArgs>(BufferChanged, UpdateInterval, "postscript token tagger", allCallbacksMustBeDone: true);
			_buffer.Changed += this._lazyEventHandler.EventHandler;
		}

		public void Dispose() {
			_buffer.Changed -= this._lazyEventHandler.EventHandler;
		}

		public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

		/// <summary>
		/// The event handler for buffer changes
		/// When the buffer changes, check to see if any of the edits were in a paragraph with multi-line tokens.
		/// If so, we need to send out a classification changed event for those paragraphs.
		/// </summary>
		void BufferChanged(object sender, TextContentChangedEventArgs e) {
			// If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
			if (e.After != this._buffer.CurrentSnapshot) {
				return;
			}
			// if it has been less than updateInterval that we have been called, ignore the event handler call
			ITextSnapshot newSnapshot = e.After;

			// First update _lineCache so its size matches snapshot.LineCount
			foreach (ITextChange change in e.Changes) {
				if (change.LineCountDelta > 0) { // lines were added   (Gets the difference in the number of lines in the snapshot that results from this change)
					int line = e.Before.GetLineFromPosition(change.OldPosition).LineNumber; // Newposition: the position of the text change in the snapshot immediately after the change.
//					// I do not know why, but line can go beyond the size of the current _lineCache
//					line = Math.Min(line, _lineCache.Count - 1);
					_lineCache.InsertRange(line, Enumerable.Repeat(new StartState(LineState.SingleLine, 0), change.LineCountDelta));
				} else if (change.LineCountDelta < 0) { // lines were removed
					int line = e.Before.GetLineFromPosition(change.OldPosition).LineNumber;
//					line = Math.Min(line, _lineCache.Count - 1);
					_lineCache.RemoveRange(line, -change.LineCountDelta);
				}
			}

			// Now that _lineCache is the appropriate size we can safely start rescanning.
			// If we hadn't updated _lineCache, then rescanning could walk off the edge.
			var changedSpans = new List<SnapshotSpan>();
			foreach (ITextChange change in e.Changes) {
				ITextSnapshotLine startLine = newSnapshot.GetLineFromPosition(change.NewPosition); 
				ITextSnapshotLine endLine = newSnapshot.GetLineFromPosition(change.NewEnd);
				int lastUpdatedLine = RescanLines(newSnapshot, startLine.LineNumber, endLine.LineNumber); // this will update _lineCache
				changedSpans.Add(new SnapshotSpan(startLine.Start, newSnapshot.GetLineFromLineNumber(lastUpdatedLine).End));
			}

			_lineCacheSnapshot = newSnapshot; // replace our snapshot that goes with our _lineCache

			// propagate the changes
			EventHandler<SnapshotSpanEventArgs> tagsChanged = TagsChanged;
			if (tagsChanged != null) {
				foreach (SnapshotSpan span in changedSpans) {
					tagsChanged(this, new SnapshotSpanEventArgs(span));
				}
			}
		}

		// Returns last line updated (will be greater than or equal to lastDirtyLine).
		// does not emit tags, only GetTags does that
		private int RescanLines(ITextSnapshot snapshot, int startLine, int lastDirtyLine) {
			int currentLine = startLine;
			Tuple<LineState, int> endstate;
			bool updatedStateForCurrentLine = true;
			var scanPostscriptLine = new ScanPostscriptLine();

			// Go until we have covered all of the dirty lines and we get to a line where our new state matches the old state.
			// for the first line, the starting state will be correct (since it was correct for the previous line)
			while (currentLine < lastDirtyLine || (updatedStateForCurrentLine && currentLine < snapshot.LineCount)) {
				ITextSnapshotLine line = snapshot.GetLineFromLineNumber(currentLine);
				endstate = scanPostscriptLine.ScanLine(_lineCache[currentLine].GetBol(), line, postscriptNamesDict: _definedPostscriptNames); // does not emit tokens, just finds the state and the level of multiline strings
				// the endstate of this line, is the startstate of the next line (if there is any)
				currentLine++; // next line
//				if ((currentLine < snapshot.LineCount) && (currentLine < _lineCache.Count)) { // is valid
				if ((currentLine < snapshot.LineCount)) { // is valid
					if (!_lineCache[currentLine].Equal(endstate)) {
						updatedStateForCurrentLine = true;
						lock (_lineCache) {
							_lineCache[currentLine].SetBol(endstate);
						}
					} else {
						updatedStateForCurrentLine = false;
					}
				}
			}
			return currentLine - 1; // last line updated.
		}

		/// <summary>
		/// Parse the given span(s) and return all the tags that intersect the specified spans.
		/// </summary>
		/// <param name="spans">ordered collection of non-overlapping spans</param>
		/// <returns>unordered enumeration of tags</returns>
		public IEnumerable<ITagSpan<PostscriptTokenTag>> GetTags(NormalizedSnapshotSpanCollection spans) {
			var scanPostscriptLine = new ScanPostscriptLine();

			foreach (SnapshotSpan span in spans) {
				// If we're called on the non-current snapshot, return nothing.
				if (span.Snapshot != _lineCacheSnapshot) {
					yield break;
				}
				SnapshotPoint lineStart = span.Start;
				while (lineStart < span.End) {
					ITextSnapshotLine line = lineStart.GetContainingLine();
					var postscriptSpans = new List<Tuple<SnapshotSpan, PostscriptTokenTag>>();
					scanPostscriptLine.ScanLine(_lineCache[line.LineNumber].GetBol(), line, postscriptSpans); // do not update the next line state even if we have obtained it, because we do not propate changes
					foreach (Tuple<SnapshotSpan, PostscriptTokenTag> tuple in postscriptSpans) {
						if (tuple.Item1.IntersectsWith(span)) {
							yield return new TagSpan<PostscriptTokenTag>(tuple.Item1, tuple.Item2);
						}
					}
					// Advance to next line.
					lineStart = line.EndIncludingLineBreak;
				}
			}

		}

	}
}
